import { ExtensionType } from '../../../../extensions/Extensions';
import { generateUniformsSync } from './utils/generateUniformsSync';

import type { UniformsSyncCallback } from '../../shared/shader/types';
import type { UniformGroup } from '../../shared/shader/UniformGroup';
import type { System } from '../../shared/system/System';
import type { GlRenderingContext } from '../context/GlRenderingContext';
import type { WebGLRenderer } from '../WebGLRenderer';
import type { GlProgram, GlUniformData } from './GlProgram';

/**
 * System plugin to the renderer to manage shaders.
 * @memberof rendering
 */
export class GlUniformGroupSystem implements System
{
    /** @ignore */
    public static extension = {
        type: [
            ExtensionType.WebGLSystem,
        ],
        name: 'uniformGroup',
    } as const;

    /**
     * The current WebGL rendering context.
     * @member {WebGLRenderingContext}
     */
    protected gl: GlRenderingContext;

    /** Cache to holds the generated functions. Stored against UniformObjects unique signature. */
    private _cache: Record<string, UniformsSyncCallback> = {};
    private _renderer: WebGLRenderer;

    private _uniformGroupSyncHash: Record<string, Record<string, UniformsSyncCallback>> = {};

    /** @param renderer - The renderer this System works for. */
    constructor(renderer: WebGLRenderer)
    {
        this._renderer = renderer;

        this.gl = null;
        this._cache = {};
    }

    protected contextChange(gl: GlRenderingContext): void
    {
        this.gl = gl;
    }

    /**
     * Uploads the uniforms values to the currently bound shader.
     * @param group - the uniforms values that be applied to the current shader
     * @param program
     * @param syncData
     * @param syncData.textureCount
     */
    public updateUniformGroup(group: UniformGroup, program: GlProgram, syncData: { textureCount: number }): void
    {
        const programData = this._renderer.shader._getProgramData(program);

        if (!group.isStatic || group._dirtyId !== programData.uniformDirtyGroups[group.uid])
        {
            programData.uniformDirtyGroups[group.uid] = group._dirtyId;

            const syncFunc = this._getUniformSyncFunction(group, program);

            syncFunc(programData.uniformData, group.uniforms, this._renderer, syncData);
        }
    }

    /**
     * Overridable by the pixi.js/unsafe-eval package to use static syncUniforms instead.
     * @param group
     * @param program
     */
    private _getUniformSyncFunction(group: UniformGroup, program: GlProgram): UniformsSyncCallback
    {
        return this._uniformGroupSyncHash[group._signature]?.[program._key]
            || this._createUniformSyncFunction(group, program);
    }

    private _createUniformSyncFunction(group: UniformGroup, program: GlProgram): UniformsSyncCallback
    {
        const uniformGroupSyncHash = this._uniformGroupSyncHash[group._signature]
            || (this._uniformGroupSyncHash[group._signature] = {});

        const id = this._getSignature(group, program._uniformData, 'u');

        if (!this._cache[id])
        {
            this._cache[id] = this._generateUniformsSync(group, program._uniformData);
        }

        uniformGroupSyncHash[program._key] = this._cache[id];

        return uniformGroupSyncHash[program._key];
    }

    private _generateUniformsSync(group: UniformGroup, uniformData: Record<string, GlUniformData>): UniformsSyncCallback
    {
        return generateUniformsSync(group, uniformData);
    }

    /**
     * Takes a uniform group and data and generates a unique signature for them.
     * @param group - The uniform group to get signature of
     * @param group.uniforms
     * @param uniformData - Uniform information generated by the shader
     * @param preFix
     * @returns Unique signature of the uniform group
     */
    private _getSignature(group: UniformGroup, uniformData: Record<string, any>, preFix: string): string
    {
        const uniforms = group.uniforms;

        const strings = [`${preFix}-`];

        for (const i in uniforms)
        {
            strings.push(i);

            if (uniformData[i])
            {
                strings.push(uniformData[i].type);
            }
        }

        return strings.join('-');
    }

    /** Destroys this System and removes all its textures. */
    public destroy(): void
    {
        this._renderer = null;
        this._cache = null;
    }
}
